/* *************************************************
vortex offline library v0.2
copyright 2007 - vortex software - all rights reserved

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. Neither the name of the project nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.



includes:
	vortex.offline - provides offline application access, syncing, etc
	vortex.storage - provides generic offline storage capabilities
	vortex.ajax - simple ajax library used by the vortex.offline module to check network availability
	vortex.utils - common helper functions
	vortex.utils.json - json conversion functions

CREDIT:
the vortex.offline and vortex.storage libraries wrap and rely on the Google Gears broswer
plug-in (http://gears.google.com/)

much of the functionality of the vortex.offline and vortex.storage libraries were inspired by
by Brad Neuberg's work on the dojo.offline library (http://dojotoolkit.org/offline). this is not 
intended as a replacement for that great library, but intended to be used when you dont need the 
entire dojo framework or are working with another javascript framework that does not already have 
offline support built in.

the vortex.utils.json library is taken from the following json proposal
(http://www50.brinkster.com/zlib13/demos/json/json-proposal-demo.htm)

************************************************* */

// create our own namespace so we can play nice with other libraries
if (!vortex) {
	var vortex = {};
}

/* *************************************************
vortex.offline

- first things first. you must call the init() method first, passing at least the application name.
you can also optionally pass in any configuration values (see below) as well as a list of resources
(urls) to cache (these will be *in addition* to any auto-discovered files unless you set the autoDiscovery
configuration value to false, in which case *only* these urls will be cached)

- configuration values:
	autoRefresh -			if true, all cached resources will be updated automatically when the application is online.
							if false, the resources are never updated after being initally cached.
							defaults to true.
	autoSync -				if true, the application will automatically try to sync when it comes back online.
							if false, you will have to manually call the sync method to synchronize changes.
							defaults to true.
	autoDiscovery -			if true, all supported resource types (css, images, etc) will automatically be cached for offline use.
							if false, you must specify which resources should be cached.
							defaults to true.
	networkPollInterval -	the amount of time (in seconds) to poll the network to see if the application is online or offline.
							defaults to 15 seconds.
	networkCheckUrl -		the url used to check network status. since the application uses an xhr request to determine connectivity,
							you should use a relative url or url that will not be restricted by the cross-domain policy.
							defaults to 'network_check.txt'
	networkTimeout -		the amount of time (in seconds) to wait for the networkCheckUrl before giving up and assuming the application is offline.
							defaults to 5 seconds.

 - the following events can be overridden to perform any custom actions that your application may require:
	onInitialized() - called when the offline features have fully been initialized (only if offline support is available)
	onNoOfflineSupport() - called once during initialization if offline support is not available (Google Gears not installed or not allowed to run)
	onNetworkStatusChange() - called whenever the network status changes from online to offline or vice-versa
	onFileCaptured(url, success, captureId) - called when a resource is captured
	onBeforeSync() - when the sync method is called, this event fires before any updates are performed
	onSyncComplete() - called once the sync process is completed
	onReplayAction(action) - during a sync operation, this is called for each action being replayed. this method should return true if the replay is successful, false otherwise

 - other methods
	discoverFiles() - scans the application and returns a list of referenced resources (images, css, etc).
					  if autoDiscover is true, this is the list of files that will be cached for the application.
					  if autoDiscover is false, you can use this method to get a list of files to add/remove from to create your manual list
					  note that the following ARE NOT automatically discovered:
						- dynamically generated scripts or content
						- resources referenced by <object> or <embed> tags
						- ActiveX controls or Java Applets
	isOffline() - returns true if the application is currently offline
	isOnline() - return true if the application is currently online
	hasOfflineSupport() - returns true if the offline components are available (Google Gears installed and allowed to run). return false otherwise.
	sync() - replays any recorded actions to keep the server in sync.
			 if autoSync is true, this method is automatically called when the application goes back online.
			 if autoSync is false, you must manually call this method to replay any recorded actions.
	recordAction(action) - records an action taken by the user or application so that it can be played back later (used to keep the server in sync)

************************************************* */
vortex.offline = function(){
	var applicationName = "";
	var applicationUrl = "";
	var storeName = "";
	var error = "";
	var initialized = false;
	var offlineSupport = false;
	var offline = null;
	var localserver;
	var store;
	var filesToCapture;
	var isReplaying = false;

    var autoRefresh = true;
	var autoSync = true;
	var autoDiscovery = true;
	var networkPollInterval = 15;
	var networkCheckUrl = "network_check.txt";
	var networkTimeout = 5;
	var dbNamespace = "__vortex__offline__";

	// private functions
	var addUrl = function(url){
		if(vortex.utils.sameDomain(url, window.location.href))
			filesToCapture[filesToCapture.length] = url;
	}

	var captureCallback = function(url, success, captureId){
		// do any file-specific stuff here
		
		// also call the public event to let the application hook in here if desired
		vortex.offline.onFileCaptured(url, success, captureId);
	}
	
	var checkNetworkAvailability = function(){
		// make xhr request to check for network connection
		vortex.ajax.setRequestTimeout(networkTimeout);
		vortex.ajax.getUrl(networkCheckUrl + "?x=" + Math.random(), function(url, xhr) {
			var previouslyOffline = offline;
			var status = 0;
			try{status = (xhr && xhr.status);}catch(err){}
			if(status == 200)
				offline = false;
			else
				offline = true;


/* uncomment this if you want to test on your localhost, where it is hard to go 'offline'
//debug
var prefix = "http://localhost";
var isLocal = (applicationUrl.substr(0,prefix.length)==prefix);
if(!offline && isLocal && xhr.responseText != "1")
    offline = true;
*/

			if(previouslyOffline != offline || !initialized){
				// handle any status changes
				networkStatusChange(offline);
				
				// if we are just initializing, fire the public event
				if(!initialized){
					initialized = true;
					vortex.offline.onInitialized();
				}

				// also call the public event to let the application hook in here if desired
				vortex.offline.onNetworkStatusChange(offline);
			}
			
			// set up to poll again
			window.setTimeout(checkNetworkAvailability, networkPollInterval * 1000);
		});
	}
	
	var networkStatusChange = function(offline){
		if(!offline && (autoRefresh || !initialized)){
			// we dont want to remove the store and re-create it (although that would be easier) 
			// because we might lose the connection again before all of the files are refreshed. 
			// instead, we do each file individually
			if(filesToCapture && filesToCapture.length){
				for(var f=0;f<filesToCapture.length;f++){
					var fileName = filesToCapture[f];
					store.remove(fileName);
					store.capture(fileName, captureCallback);
				}
			}
		}
		
		if(!offline && autoSync){
			vortex.offline.sync();
		}
	}
	
	var replayActions = function(){
		if(!isReplaying){
			isReplaying = true;
			var actions = vortex.storage.getAll(dbNamespace);

			// replay each action
			for(var key in actions){
				var action = actions[key];
				var success = vortex.offline.onReplayAction(action);
				if(success){
					// remove action from list
					vortex.storage.remove(key, dbNamespace);
				}
				else {
					// what if it fails?
				}
			}
			isReplaying = false;
		}
	}
	
	var parseStylesheets = function(sheet, files){
		try{
			// handle @import recursively (IE only)
			if(sheet.imports && sheet.imports.length){
				for(var i=0;i<sheet.imports.length;i++){
					files.push(sheet.imports[i].href);
					parseStylesheets(sheet.imports[i], files);
				}
			}
			
			var rules = (sheet.cssRules || sheet.rules);
			if(rules && rules.length){
				for(var r=0;r<rules.length;r++){
					var rule = rules[r];

					// handle @imports recursively (FF)
					if(rule.styleSheet) parseStylesheets(rule.styleSheet, files);

					var text = (rule.cssText || rule.style.cssText);
					if(text){
						var matches = text.match(/url\(\s*([^\) ]*)\s*\)/i);
						if(matches){
							for(var j = 1; j < matches.length; j++){
								files.push(matches[j]);
							}
						}
					}
				}
			}
		}catch(err){
		}
	}
	
	// public interface
    return {
		// public events that can be overridden per application
		onInitialized : function() {},
		onNoOfflineSupport : function() {},
		onNetworkStatusChange : function(isNowOffline) {},
		onFileCaptured : function(url, success, captureId) {},
		onBeforeSync : function() {},
		onSyncComplete : function() {},
		onReplayAction : function(action) {return true;},

		// public methods
		init : function(appName, config, files) {
			// get basic application information
			applicationName = appName;
			applicationUrl = window.location.href;
			storeName = vortex.utils.replaceNonAlphaNumericCharacters(applicationUrl);
			dbNamespace += storeName;

			// handle overriding any configuration details
			if(config){
				if(config.autoRefresh) autoRefresh = config.autoRefresh;
				if(config.autoSync) autoSync = config.autoSync;
				if(config.autoDiscovery) autoDiscovery = config.autoDiscovery;
				if(config.networkPollInterval) networkPollInterval = config.networkPollInterval;
				if(config.networkCheckUrl) networkCheckUrl = config.networkCheckUrl;
				if(config.networkTimeout) networkTimeout = config.networkTimeout;
			}

			// initialize offline support
			if(google.gears) {
				try{
					// if the user denies google gears the rights to run, this will fail
					localserver = google.gears.factory.create('beta.localserver', '1.0');
					store = localserver.createStore(storeName);
					offlineSupport = true;
				}
				catch(err){}
			}
			if(!offlineSupport){
				vortex.offline.onNoOfflineSupport();
				return;
			}

			// determine files needed to run offline
			filesToCapture = [];
			if(autoDiscovery){
				// find files and add them to the list
				var autoFiles = this.discoverFiles();
				// add to list
				if(autoFiles && autoFiles.length && autoFiles.length > 0) {
					for(var f=0;f<autoFiles.length;f++){
						addUrl(autoFiles[f]);
					}
				}
			}
			// add any additional files specified
			if(files && files.length && files.length > 0) {
				for(var f=0;f<files.length;f++){
					addUrl(files[f]);
				}
			}

			// do an initial network check and then start polling at regular intervals
			// so we know when we go on/offline
			checkNetworkAvailability();

			return true;
		},
		
		discoverFiles : function() {
			// return a list of resources required by this page, including:
			//    - the page calling this script
			//    - any referenced javascript files
			//    - any referenced stylesheets (including @import)
			//    - any inline images
			//    - any images referenced in the linked stylesheets
			// note that the following ARE NOT automatically discovered:
			//    - dynamically generated scripts or content
			//    - resources referenced by <object> or <embed> tags
			//    - ActiveX controls or Java Applets
			var elements;
			var e;
			var i;
			var files = [];
			
			// this page
			files.push(window.location.href);
			
			// referenced scripts
			elements = document.getElementsByTagName("script");
			for(i=0;i<elements.length;i++){
				e = elements[i];
				if(e.src && e.src != "") files.push(e.src);
			}
			
			// referenced inline images
			elements = document.getElementsByTagName("img");
			for(i=0;i<elements.length;i++){
				e = elements[i];
				if(e.src && e.src != "") files.push(e.src);
			}
			
			// referenced stylesheets
			elements = document.getElementsByTagName("link");
			for(i=0;i<elements.length;i++){
				e = elements[i];
				if(e.rel && e.rel.toLowerCase() == "stylesheet") files.push(e.getAttribute("href"));
			}
			
			// @import and url()
			if(document.styleSheets && document.styleSheets.length){
				for(var i=0;i<document.styleSheets.length;i++){
					parseStylesheets(document.styleSheets[i], files);
				}
			}
			
			return files;
		},
		
		isOnline : function() {
			return !offline;
		},
		
		isOffline : function() {
			return offline;
		},
		
		hasOfflineSupport : function() {
			return offlineSupport;
		},

		sync : function() {
			// fire public event
			vortex.offline.onBeforeSync();

			// replay any recorded actions
			replayActions();
			
			// fire the public event
			vortex.offline.onSyncComplete();
		},
		
		recordAction : function(action){
			var key = new Date().toString();
			vortex.storage.put(key, action, dbNamespace);
		}
    }
}();

/* *************************************************
vortex.storage

- the vortex.storage object is a singleton that is automatically initialized upon access, so 
you do not have to initialize it and can access it at any time by using vortex.storage.method()

- all items are stored in a common database and table. if you want to segregate your data, use
the 'namespace' parameter to keep different types of data or different parts of your application
stored individually

- if Google Gears is not installed or not allowed to run, the storage methods will not do anything.
you can use vortex.storage.isAvailable() to check if storage capabilites are available.

- all data is stored as a JSON representation of the object. if you pass in objects with circular
references, it will blow up.

- methods
	get(key, namespace) - retrives the object with the given key and namespace
	getAll(namespace) - retrieves a recordset containing all records in the given namespace
	put(key, val, namespace) - save the val object using the specified key and namespace.
							   if an item already exists with the given key and namespace, it will be overwritten.
	remove(key, namespace) - removes the item with the given key and namespace
	clear(namespace) - removes all items currently saved in the given namespace
	isAvailable() - returns true if the storage provider is available (Google Gears is installed and allowed to run), otherwise returns false

************************************************* */
vortex.storage = function(){
	var isAvailable = false;
	var dbName = "__vortex";
	var tableName = "__vortex__storage";
	var db = null;
	
	// initialize offline support
	if(google.gears) {
		try{
			db = google.gears.factory.create('beta.database', '1.0');
			db.open(dbName);

			db.execute("CREATE TABLE IF NOT EXISTS " + tableName + "( "
						+ " namespace TEXT, "
						+ " key TEXT, "
						+ " value TEXT "
						+ ")"
					);
			db.execute("CREATE UNIQUE INDEX IF NOT EXISTS namespace_key_index" 
						+ " ON " + tableName
						+ " (namespace, key)"
					);
			
			isAvailable = true;
		}
		catch(err){}
	}

	return {
		get : function(key, namespace){
			if(isAvailable){
				var val = null;

				var rs = db.execute("SELECT * FROM " + tableName
							+ " WHERE namespace = ? AND "
							+ " key = ?",
							[namespace, key]);
				if(rs.isValidRow()) {
					val = rs.field(2);
					return vortex.utils.json.parse(val);
				}
				else {
					return null;
				}
			}
			else
				return null;
		},

		getAll : function(namespace){
			if(isAvailable){
				var list = [];
				var rs = db.execute("SELECT * FROM " + tableName
							+ " WHERE namespace = ?",
							[namespace]);
				while (rs.isValidRow()) {
					list[rs.field(1)] = vortex.utils.json.parse(rs.field(2));
					rs.next();
				}
				rs.close();
				return list;
			}
			else
				return null;
		},

		put : function(key, val, namespace){
			if(isAvailable){
				var value = vortex.utils.json.stringify(val);
				
				vortex.storage.remove(key, namespace);
				db.execute("INSERT INTO " + tableName
						+ " VALUES (?, ?, ?)",
						[namespace, key, value]);
				return true;
			}
			else
				return false;
		},
		
		remove : function(key, namespace){
			if(isAvailable){
				db.execute("DELETE FROM " + tableName
							+ " WHERE namespace = ? AND key = ?",
							[namespace, key]);
				return true;
			}
			else
				return false;
		},
		
		clear : function(namespace){
			if(isAvailable){
				db.execute("DELETE FROM " + tableName
							+ " WHERE namespace = ?",
							[namespace]);
				return true;
			}
			else
				return false;
		},
		
		isAvailable : function(){
			return isAvailable;
		}
	}
}();

/* *************************************************
vortex.ajax

- this is a simple xhr wrapper that is mainly used by the vortex.offline module to check network connetivity.
as such, it is not intended to be a replacement for a your regular full-featured ajax library as much as it 
is a 'quick and easy' way to perform simple xhr calls without any unnecessary functionality

************************************************* */
vortex.ajax = function(){
	var timeout = 5;
	
	var doCallback = function(xhr, callback, callbackArguments){
		// we replace the second argument (callback function) with the xhr object)
		// kind of cheesy, but it is nice and easy and works well since this is not
		// a full-feldged ajax library
		callbackArguments[1] = xhr;
		callback.apply(this, callbackArguments);
		xhr = null;
	}

	return {
		setRequestTimeout : function(t){
			timeout = t;
		},

		getUrl : function(url, callback){
			var xhr = null;
			var callbackArguments = arguments;

			// branch for native XMLHttpRequest object
			if(window.XMLHttpRequest) {
    			try {
					xhr = new XMLHttpRequest();
				} catch(e) {
					xhr = null;
				}
			// branch for IE/Windows ActiveX version
			} else if(window.ActiveXObject) {
       			try {
        			xhr = new ActiveXObject("Msxml2.XMLHTTP");
      			} catch(e) {
        			try {
          				xhr = new ActiveXObject("Microsoft.XMLHTTP");
        			} catch(e) {
          				xhr = null;
        			}
				}
			}
			
			if(xhr) {
				xhr.onreadystatechange = function () {
												if(xhr && xhr.readyState == 4) {
													// cancel the timeout timer
													window.clearTimeout(timer);
													
													// handle the callback
													doCallback(xhr, callback, callbackArguments);
												}
											}
			}
	
			if(xhr) {
				var method = "GET";
				var contentType = "text/plain";
				var data = "";
				xhr.open(method, url, true);
				xhr.setRequestHeader("Content-Type", contentType);
				xhr.send(data);
				
				timer = window.setTimeout(function() {
					xhr.onreadystatechange = function(){};
					try{xhr.abort();}catch(err){}
					// even though we timed out, lets handle this case like a successful error
					doCallback(xhr, callback, callbackArguments);
				}, timeout * 1000);
			}
		}
	}
}();

/* *************************************************
vortex.utils & vortex.utils.json

- just a hodge-podge of utility functions used by the other vortex libraries

- the json module includes some regexs that look like unterminated strings, so some minification tools might choke on it

- if you would prefer to use another json library, you can override the following methods:
	vortex.utils.json.stringify(obj) - where obj is the object to convert. returns a json-formatted string
	vortex.utils.json.parse(json) - where json is the json-formatted string to parse. returns a javascript object

************************************************* */
vortex.utils = function(){
	return {
		startsWith : function(input, prefix){
			return (input.substr(0,prefix.length)==prefix);
		},

		replaceNonAlphaNumericCharacters : function(input){
			var output = input.replace(/[^0-9A-Za-z_]/g, "_");
			return output;
		},
		
		sameDomain : function(url1, url2){
			url1 = url1.toLowerCase();
			url2 = url2.toLowerCase();

			// exact same url == match
			if(url1 == url2) return true;
			// relative url == match
			if(!vortex.utils.startsWith(url1, "http://") && !vortex.utils.startsWith(url1, "https://")) return true;
			// same domain == match
			var index1 = url1.indexOf("/", 8);
			var index2 = url2.indexOf("/", 8);
			if(index1 == -1) index1 = url1.length - 1;
			if(index2 == -1) index2 = url2.length - 1;
			var domain1 = url1.substring(0, index1);
			var domain2 = url2.substring(0, index2);
			if(domain1 == domain2) return true;
			// all else == fail
			return false;
		},
		
		json : function() {
			// Test for modern browser (any except IE5).
			var JS13 = ('1'.replace(/1/, function() { return ''; }) == '');

			// CHARS array stores special strings for encodeString() function.
			var CHARS = {
				'\b': '\\b',
				'\t': '\\t',
				'\n': '\\n',
				'\f': '\\f',
				'\r': '\\r',
				'\\': '\\\\',
				'"' : '\\"'
			};

			for (var i = 0; i < 32; i++) {
				var c = String.fromCharCode(i);
				if (!CHARS[c]) CHARS[c] = ((i < 16) ? '\\u000' : '\\u00') + i.toString(16);
			};
			  
			function encodeString(str) {
				if (!/[\x00-\x1f\\"]/.test(str)) {
				  return str;
				} else if (JS13) {
				  return str.replace(/([\x00-\x1f\\"])/g, function($0, $1) {
					return CHARS[$1];
				});
				} else {
				var out = new Array(str.length);
				for (var i = 0; i < str.length; i++) {
					var c = str.charAt(i);
					out[i] = CHARS[c] || c;
				}
				return out.join('');
				}
			};

			return {
				stringify : function (arg) {
					switch (typeof arg) {
						case 'string'   : return '"' + encodeString(arg) + '"'; // break command is redundant here and below.
						case 'number'   : return String(arg);
						case 'object'   : 
						if (arg) {
							var out = [];
							if (arg instanceof Array) {
							for (var i = 0; i < arg.length; i++) {
								var json = this.stringify(arg[i]);
								if (json != null) out[out.length] = json;
							}
							return '[' + out.join(',') + ']';
							} else {
							for (var p in arg) {
								var json = this.stringify(arg[p]);
								if (json != null) out[out.length] = '"' + encodeString(p) + '":' + json;
							}
							return '{' + out.join(',') + '}';
							}
						}
						return 'null'; // if execution reaches here, arg is null.
						case 'boolean'  : return String(arg);
						// cases function & undefined return null implicitly.
					}
				},

				parse: function (text) {
					try {
						return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(text.replace(/"(\\.|[^"\\])*"/g, ''))) && eval('(' + text + ')');
					} catch (e) {
						return false;
					}
				}
			}
		}()
	}
}();
